//+---------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation
//
// File: CacheDataProvider.cs
//
// Contents: Implementation of IDataProvider - cached data provider
//
// Created by: Andrew Lin (yunlin) April 22 2008
//
//----------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.MSAS.ManagedAPI;
using MRS.FrontEnd.BO;

namespace Microsoft.Reputation.Services
{
    internal class CacheLookupState
    {
        public readonly IEnumerable<DPLookupRequest> Requests;

        public uint Status;
        public DPLookupResponse[] CachedLookupResponses;

        public long latestOverrideStamp;
        public long dataProviderLatency;

        public CacheLookupState(
            IEnumerable<DPLookupRequest> requests,
            DPLookupResponse[] cachedLookupResponses)
        {
            this.Requests = requests;
            this.CachedLookupResponses = cachedLookupResponses;
        }
    }

    internal class CacheLookupAsyncResult
        : AsyncResultOwnState<DPLookupResponse[], CacheLookupState>
    {
        public CacheLookupAsyncResult(CacheLookupState ownState, AsyncCallback hostCallback, object hostState)
            : base(ownState, hostCallback, hostState)
        {
        }
    }

    public class CacheDataProvider : IDisposable, IDataProvider
    {
        private static readonly DataType[] s_allDataTypes = (DataType[])Enum.GetValues(typeof(DataType));

        //
        // Interface to the underlying persistent data provider
        //
        internal static IDataProvider PersistentDataProvider { get; private set; }
        internal static ICacheProvider MemoryCacheProvider { get; private set; }
        private static IMsasApiRequestProcessor DataBlockProcessor { get; set; }

        public CacheDataProvider()
        {
            Type theDataProvider = null;

            try
            {
                theDataProvider = Type.GetType(DeliveryServiceConfig.DataProviderTypeDefault);

                if (theDataProvider != null)
                {
                    ConstructorInfo constructor = theDataProvider.GetConstructor(Type.EmptyTypes);

                    if (constructor != null)
                    {
                        PersistentDataProvider = constructor.Invoke(new object[0]) as IDataProvider;
                    }
                }

                Type theCacheProvider = Type.GetType(DeliveryServiceConfig.CacheProviderType);
                if (theCacheProvider != null)
                {
                    ConstructorInfo constructor = theCacheProvider.GetConstructor(Type.EmptyTypes);

                    if (constructor != null)
                    {
                        MemoryCacheProvider = constructor.Invoke(new object[0]) as ICacheProvider;
                    }
                }

                DataBlockProcessor = new MsasApiRequestProcessor();
            }
            catch (Exception ex)
            {
                TraceUtility.TraceCatchException(Tracing.Source, ex);
                TraceUtility.TraceAssert(Tracing.Source, false, "Failed to initialize specified IDataProvider");
                TraceUtility.TraceThrowException(Tracing.Source, ex);
                throw;
            }
        }

        /// <summary>
        /// Poor man's dependency injection.  Used by unit tests to prime the object for use.
        /// </summary>
        /// <param name="dataProvider"></param>
        /// <param name="cacheProvider"></param>
        /// <param name="requestProcessor"></param>
        internal CacheDataProvider(
            IDataProvider dataProvider, 
            ICacheProvider cacheProvider, 
            IMsasApiRequestProcessor requestProcessor)
        {
            PersistentDataProvider = dataProvider;
            MemoryCacheProvider = cacheProvider;
            DataBlockProcessor = requestProcessor;
        }

        public void Dispose()
        {
            if (MemoryCacheProvider != null)
            {
                MemoryCacheProvider.Dispose();
                MemoryCacheProvider = null;
            }
        }

        private static bool TryGetLookupMatchedEntries(
            DPLookupRequest lookupRequest,
            out List<UnpackedObject> matchedEntries)
        {
            TraceUtility.TraceAssert(Tracing.Source, lookupRequest != null, "lookupRequest should not be null");

            bool cacheFound = false;

            if (lookupRequest.RequestType == RequestType.UrlDataBlock)
            {
                byte[] datablock = ((DPLookupUrlDataBlock)lookupRequest).DataBlock;

                MsasDataBlockProcessor processor = new MsasDataBlockProcessor(DataBlockProcessor, datablock, MemoryCacheProvider);
                CacheRequestProcessorContext context = processor.ProcessRequestForCache(lookupRequest.ObjectType);

                matchedEntries = context.FoundObjects;
                // If the SearchDB flag is true then we know that we did _not_ find
                // everything in the cache that we were looking for so cacheFound
                // is set to false so that we go dig in the database for the remainder
                // of the objects.
                cacheFound = (context.SearchDB == false);
            }
            else
            {
                DPLookupUrlOverrides overridesRequest = (DPLookupUrlOverrides)lookupRequest;
                cacheFound = MemoryCacheProvider.GetMatchedEntries(
                    overridesRequest.Overrides,
                    out matchedEntries);
            }

            return cacheFound;
        }
        
        private static DPLookupResponse[] FindObjectsInCache(
            DPLookupRequest[] lookupRequests,
            out DPLookupRequest[] cacheMissObjects)
        {
            List<DPLookupRequest> missedObjects = new List<DPLookupRequest>(lookupRequests.Length);
            DPLookupResponse[] responseArray = new DPLookupResponse[lookupRequests.Length];

            for (uint i = 0; i < lookupRequests.Length; i++)
            {
                List<UnpackedObject> matchedEntries;                

                // Go dig in the cache to see if we have what we are looking for in 
                // this request.
                if (TryGetLookupMatchedEntries(lookupRequests[i], out matchedEntries))
                {
                    // We found items in the cache mark a hit if it was for a regular query (non-override)
                    if (lookupRequests[i].RequestType == RequestType.UrlDataBlock)
                    {
                        PerfCounters.LookupObjectCacheHit();
                    }

                    responseArray[i] = new DPLookupResponse
                    {
                        Matches = matchedEntries
                    };
                }
                else
                {
                    // Not found in the cache so we want to put it in a 
                    // list for the next layer down to handle.  We only mark misses for 
                    // regular queries, not override queries
                    if (lookupRequests[i].RequestType == RequestType.UrlDataBlock)
                    {
                        PerfCounters.LookupObjectCacheMiss();
                    }

                    responseArray[i] = null;
                    missedObjects.Add(lookupRequests[i]);
                }
            }

            cacheMissObjects = missedObjects.ToArray();

            return responseArray;
        }

        #region Generic Lookup handling

        private static void GetLookupResult(IAsyncResult asyncRes)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                CacheLookupAsyncResult lookupAsyncResult = (CacheLookupAsyncResult)asyncRes.AsyncState;

                //List<DPLookupRequest> requests = lookupAsyncResult.OwnState.Requests;

                try
                {
                    DPLookupResponse[] persistentResponseArray = PersistentDataProvider.EndLookupObjects(
                        out lookupAsyncResult.OwnState.Status,
                        out lookupAsyncResult.OwnState.dataProviderLatency,
                        asyncRes);

                    TraceUtility.TraceAssert(Tracing.Source, persistentResponseArray != null, "Response array expected not to be null CacheDataProvider.GetLookupResult");

                    DPLookupResponse[] cacheResponses = ((CacheLookupState)lookupAsyncResult.OwnState).CachedLookupResponses;

                    DPLookupResponse[] persistentResponses = persistentResponseArray;
                    int persistentResponseCounter = 0;

                    for (int i = 0; i < cacheResponses.Length; i++)
                    {
                        if (cacheResponses[i] != null)
                        {
                            //
                            // Skip. Matches have been found in cache.
                            //
                            continue;
                        }

                        MemoryCacheProvider.CacheMatchedEntries(persistentResponseArray[persistentResponseCounter].Matches);

                        cacheResponses[i] = persistentResponseArray[persistentResponseCounter];
                        persistentResponseCounter++;
                    }

                    lookupAsyncResult.SetComplete(cacheResponses, asyncRes.CompletedSynchronously);
                }
                catch (Exception ex)
                {
                    TraceUtility.TraceCatchException(Tracing.Source, ex);
                    lookupAsyncResult.SetComplete(ex, asyncRes.CompletedSynchronously);
                }
            }
        }

        private static IAsyncResult InternalBeginLookupData(
            IEnumerable<DPLookupRequest> requests,
            AsyncCallback callback,
            object state)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                DPLookupRequest[] cacheMissLookupRequests;

                DPLookupResponse[] lookupResponses = FindObjectsInCache(requests.ToArray(), out cacheMissLookupRequests);

                CacheLookupState cacheLookupState = new CacheLookupState(
                    requests,
                    lookupResponses);

                CacheLookupAsyncResult asyncResult = new CacheLookupAsyncResult(
                    cacheLookupState,
                    callback,
                    state);

                if (cacheMissLookupRequests.Length == 0)
                {
                    //
                    // Everything found in cache, so we complete synchronously
                    //
                    cacheLookupState.Status = StatusCodes.OK;
                    asyncResult.SetComplete(lookupResponses, true);
                }
                else
                {
                    //
                    // We had some/all misses in the cache so we need to get the rest from the database.
                    // 
                    PersistentDataProvider.BeginLookupObjects(
                        cacheMissLookupRequests,
                        GetLookupResult,
                        asyncResult);
                }

                return asyncResult;
            }
        }

        private static DPLookupResponse[] InternalEndLookupObjects(
            out uint operationStatus,
            out long dataProviderLatency,
            IAsyncResult asyncResult)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                dataProviderLatency = 0;
                if (asyncResult == null)
                {
                    throw new ArgumentNullException("asyncResult");
                }

                DPLookupResponse[] result = null;
                CacheLookupState lookupState = null;
                CacheLookupAsyncResult lookupAsyncResult = null;

                try
                {
                    lookupAsyncResult = (CacheLookupAsyncResult)asyncResult;
                    lookupState = lookupAsyncResult.OwnState;

                    result = lookupAsyncResult.EndInvoke();

                    operationStatus = lookupState.Status;
                    dataProviderLatency = lookupState.dataProviderLatency;
                }
                catch (Exception ex)
                {
                    result = null;
                    operationStatus = StatusCodes.Error;

                    TraceUtility.TraceCatchException(Tracing.Source, ex);

                    if (DeliveryServiceHelpers.IsFatalException(ex))
                    {
                        TraceUtility.TraceThrowException(Tracing.Source, ex);
                        throw;
                    }
                }

                return result;
            }
        }

        #endregion

        #region IDataProvider Members

        public IAsyncResult BeginLookupObjects(
            IEnumerable<DPLookupRequest> requests,
            AsyncCallback callback,
            object state)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                return InternalBeginLookupData(requests, callback, state);
            }
        }

        public DPLookupResponse[] EndLookupObjects(
            out uint operationStatus,
            out long dataProviderLatency,
            IAsyncResult asyncResult)
        {
            using (TraceUtility.TraceMethod(Tracing.Source))
            {
                return InternalEndLookupObjects(out operationStatus, out dataProviderLatency, asyncResult);
            }
        }

        /// <summary>
        /// Gets overrides that will be returned to the calling clients from the layer below
        /// </summary>
        /// <param name="latestOverrideStamp">The last stamp gotten from the database</param>
        /// <returns>A response with a list of overrides if there are any</returns>
        public DPOverrideResponse GetOverrides(long latestOverrideStamp)
        {
            return PersistentDataProvider.GetOverrides(latestOverrideStamp);
        }

        #endregion
    }
}
